package com.sample.snl.frontend.SyntaxAnalysis;

import com.sample.snl.frontend.LexicalAnalysis.Token;
import com.sample.snl.frontend.LexicalAnalysis.TokenType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.swing.*;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
import java.util.Vector;

public class LL1Parser {
    private List<Token> tokenList;
    public List<String> errorList;
    public SyntaxTree parsingTree;
    private transient int indexOfCurrentToken = 0;
    private Token errorToken = new Token(TokenType.ERROR);

    private static final Logger LOG = LoggerFactory.getLogger(LL1Parser.class);
    private Token lastRead = errorToken;


    public SyntaxTree getParsingTree() {
        return parsingTree;
    }

    /**
     * The constructor requires the token list derived from the lexical analysis
     *
     * @param tokens
     */
    public LL1Parser(List<Token> tokens) {
        this.tokenList = tokens;
        this.errorList = new ArrayList<>();
    }

    /**
     * The core part of the LL1 parsing procedure
     * Generate the parsing tree and return, if any, the error list,
     */
    public void parse() {
        Stack<Symbol> symbolStack = new Stack<>();
        // static method which returns a new non-terminal symbol
        NonTerminalSymbol startSymbol = NonTerminalSymbol.Program();
        Node root = startSymbol.getNode();
        this.parsingTree = new SyntaxTree(root);
        // push the root into stack
        symbolStack.push(startSymbol);

        // the main part of LL1 parsing procedure
        // the while loop holds when the symbol stack is not empty and
        // the first unprocessed input token is not EOF
        Symbol currentSymbol;

        Vector<Vector<String>> data = new Vector<Vector<String>>();
        Vector<String> dataTitle = new Vector<String>();
        dataTitle.add("NO.");
        dataTitle.add("Input token:");
        dataTitle.add("Symbol stack:");
        int num = 1;
        while (!symbolStack.isEmpty() && peekNextToken().getType() != TokenType.EOF) {
            String str = "";
            for (Symbol s : symbolStack) {
                //System.out.print("\t" + s.toString());
                str = str + s.toString();
            }
            Vector<String> W = new Vector<String>();
            W.add(num + "");
            num++;
            W.add(peekNextToken().toString());
            W.add(str);
            data.add(W);

            currentSymbol = symbolStack.pop();
            if (currentSymbol instanceof TerminalSymbol) {
                // try to match the terminal symbol

                // Note that the match method invokes getNextToken() method internally
                // and it is the only place where indexOfCurrentToken gets increased.
                Node n = match(((TerminalSymbol) currentSymbol).getToken().getType());

                currentSymbol.getNode().setValue(n.getValue());
            } else if (currentSymbol instanceof NonTerminalSymbol) {
                NonTerminalSymbol currentNon = (NonTerminalSymbol) currentSymbol; // downcast
                List<Symbol> rhs = lookup(currentNon, peekNextToken()); // get the production
                if (rhs == null) {
                    System.out.println("Failed to match the terminal symbol.");
                    errorList.add("Unexpected token near [" + lastRead.getLexeme() + "]. at ["
                            + lastRead.getLine() + ":" + lastRead.getColumn() + "]");
                } else {

                    // the symbols in rhs make up the children of the current node
                    Node[] children = new Node[rhs.size()];
                    for (int i = 0; i < children.length; i++) children[i] = rhs.get(i).getNode();
                    currentNon.getNode().setChildren(children);
                    // replace the current symbol in the symbol stack with its children
                    for (int i = children.length - 1; i >= 0; i--) {
                        Symbol s = rhs.get(i);
                        if (!(s instanceof TerminalSymbol && (TerminalSymbol) s == TerminalSymbol.epsilon))
                            symbolStack.push(s);
                    }
                }
            } else {
                errorList.add("Unexpected symbol.");
            }
        }

        // when the while loos terminates,
        // it means the symbol stack is empty and the input stream has only EDF unprocessed
        if (!symbolStack.isEmpty()) {
            LOG.warn("The symbol stack should be empty!");
            errorList.add("The symbol stack should be empty!");
        }
        if (getNextToken().getType() != TokenType.EOF) {
            System.out.println("Expected to meet EOF!");
            errorList.add("Expected to meet EOF!");
        }
        System.out.println("\n");

        JFrame jf = new JFrame();
        jf.setBounds(520, 60, 1000, 800);
        jf.setTitle("Syntax analysis");
        jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        JTable table = new JTable(data, dataTitle);
        table.getColumnModel().getColumn(0).setPreferredWidth(50);
        table.getColumnModel().getColumn(1).setPreferredWidth(250);
        table.getColumnModel().getColumn(2).setPreferredWidth(700);
        JScrollPane jscrollpane = new JScrollPane(table);
        jf.add(jscrollpane, BorderLayout.CENTER);
        jf.setVisible(true);
    }

    /**
     * Find the corresponding production with the given non-terminal symbol and token
     *
     * @param nonTerminalSymbol
     * @param token
     * @return the right hand side of the production
     */
    private List<Symbol> lookup(NonTerminalSymbol nonTerminalSymbol, Token token) {
        return LL1Table.valueOf(nonTerminalSymbol.getValue()).getProduction(token);
    }


    private Token getNextToken() {
        Token token = null;
        if (indexOfCurrentToken < tokenList.size()) {
            token = tokenList.get(indexOfCurrentToken++);
            lastRead = token;
        } else {
            System.out.println("No more token in the input stream.");
        }
        return token;
    }


    /**
     * Note that this method returns error token when index is out of range
     *
     * @return
     */
    private Token peekNextToken() {
        return indexOfCurrentToken < tokenList.size() ?
                tokenList.get(indexOfCurrentToken) : errorToken;
    }


    private Node node(String value) {
        return new Node(value);
    }


    private Node node() {
        return new Node("empty");
    }


    /**
     * When the top of the symbol stack is a terminal, try to match the input symbol
     *
     * @param tokenType
     * @return A tree node
     */
    private Node match(TokenType tokenType) {
        // get the input token
        Token input = getNextToken();
        Node resultTerminalNode = node(); // by default it is a empty node
        if (input == null) {
            errorList.add("Unexpected EOF");
            return resultTerminalNode;
        }

        TokenType inputTokenType = input.getType();
        if (!inputTokenType.equals(tokenType)) {
            errorList.add("Unexpected token near [" + input.getLexeme() + "]. ["
                    + tokenType.getStr() + "] expected. "
                    + " at [" + input.getLine() + ":" + input.getColumn() + "]");
        } else {
            // match! Then generate the node
            resultTerminalNode = (inputTokenType == TokenType.ID ||
                    inputTokenType == TokenType.INTC ||
                    inputTokenType == TokenType.CHARACTER) ?
                    // check whether the type of the input token is ID, INTC or character
                    // if so, replace the node's value
                    node(input.getLexeme()) : node(tokenType.getStr());
        }
        return resultTerminalNode;
    }
}
